iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
Software Development

Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app系列 第 28

Android Fragment 實戰:從生命週期、Fragment 切換到 Activity 資料傳遞

  • 分享至 

  • xImage
  •  

Day 27:Android Fragment 實戰:從生命週期、Fragment 切換到 Activity 資料傳遞

昨天我們已經接觸 Firebase,今天先把 Android App 很重要的另一塊拼圖補起來:Fragment(片段)

從今天實作可以看到,我們不再只是「一個 Activity 一個畫面」,而是讓一個 MainActivity 裡面放入不同的 Fragment,並透過按鈕在 Fragment 1 / Fragment 2 / Fragment 3 之間操作。除此之外,今天的程式也實際觀察 Fragment 生命週期,以及 Fragment 與 Activity 之間如何交換資料。你提供的 Fragment_1.kt 已完整加入 onAttach()onCreate()onCreateView()onViewCreated()onStart()onResume()onStop()onDestroyView()onDestroy()onDetach() 等生命週期紀錄。

參考資料:Android Developers-Fragments


Fragment 到底是什麼?

前面學 Android 時,我們大量使用 Activity

可以簡單想成:

Activity = 一個完整畫面
Fragment = Activity 裡面可以替換的小畫面

例如今天的畫面:

┌────────────────────────────────────┐
│             MainActivity           │
│                                    │
│ [ Frag-1 ] [ Frag-2 ] [ Frag-3 ]   │
│                                    │
│          Input data                │
│                                    │
│ ┌────────────────────────────────┐ │
│ │                                │ │
│ │          Fragment              │ │
│ │                                │ │
│ │          TextView              │ │
│ │                                │ │
│ │          ImageView             │ │
│ │                                │ │
│ └────────────────────────────────┘ │
└────────────────────────────────────┘

最大的好處就是:不用一直開新的 Activity,也能替換畫面的一部分。

這也是之後做 Bottom Navigation、Tab 分頁、平板雙欄畫面時非常重要的基礎。

參考資料:Android Developers-Fragment overview


一、先從最基本的 Fragment 開始

今天第一個練習的 Fragment_1.kt,是利用 Android Studio 建立 Fragment 後,再加入自己的元件與操作。

核心宣告:

class Fragment_1 : Fragment() {

這裡和 Activity:

class MainActivity : AppCompatActivity()

最大的差異,就是 Fragment 繼承:

Fragment()

Fragment 自己不是完整 Activity,因此它的 UI 必須透過 onCreateView() 建立。

參考資料:Android Developers-Create a fragment


二、Fragment 的 onCreateView()

今天程式裡非常重要的一段:

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {

    Log.d("main", "onCreateView-F")

    val fragView =
        inflater.inflate(R.layout.fragment_1, container, false)

    imageViewPic =
        fragView.findViewById<ImageView>(R.id.imageView_fragPic)

    textViewName =
        fragView.findViewById<TextView>(R.id.textView_fragName)

    buttonPic =
        fragView.findViewById<Button>(R.id.button_frag)

    editTextInput =
        fragView.findViewById<EditText>(R.id.editTextText_frag)

    buttonData =
        fragView.findViewById<Button>(R.id.button_FragData)

    return fragView
}

這裡的:

inflater.inflate()

就是把:

fragment_1.xml

轉成真正可以顯示的 View

所以 Fragment 的關係可以理解成:

Fragment_1.kt
      │
      │ inflate
      ▼
fragment_1.xml
      │
      ▼
    View
      │
      ▼
顯示到 Activity

findViewById() 也不是直接從 Activity 找,而是從:

fragView.findViewById()

尋找 Fragment 自己 XML 裡面的元件。

參考資料:Android Developers-Fragment lifecycle


三、Fragment 裡面也可以操作 UI

Fragment 並不是只能顯示靜態內容。

今天的範例使用:

private var imgFlag: Boolean = true

記錄目前圖片狀態。

按下按鈕:

buttonPic.setOnClickListener {

    if (imgFlag) {

        textViewName.text = "Flower 2"
        imageViewPic.setImageResource(R.drawable.flower2)
        imgFlag = false

    } else {

        textViewName.text = "Flower 1"
        imageViewPic.setImageResource(R.drawable.flower1)
        imgFlag = true
    }
}

因此每按一次:

Flower 1
   ↓
按 Button
   ↓
Flower 2
   ↓
按 Button
   ↓
Flower 1

Fragment 本身就可以管理自己的 TextViewImageViewButton 等 UI 元件。

參考資料:Android Developers-Fragments


四、今天非常重要:Fragment 生命週期

Activity 有生命週期:

onCreate()
onStart()
onResume()
onPause()
onStop()
onDestroy()

Fragment 也有自己的生命週期,而且比 Activity 更細。

今天程式特別利用:

Log.d("main", "...")

把各階段印到 Logcat。

例如:

override fun onAttach(context: Context) {
    super.onAttach(context)
    Log.d("main", "onAttach-F")
}

以及:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    arguments?.let {
        param1 = it.getString(ARG_PARAM1)
        param2 = it.getString(ARG_PARAM2)

        Log.d("main", "onCreate-F")
    }
}

建立畫面:

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {

    Log.d("main", "onCreateView-F")

    // ...
}

畫面建立完成:

override fun onViewCreated(
    view: View,
    savedInstanceState: Bundle?
) {
    super.onViewCreated(view, savedInstanceState)

    Log.d("main", "onViewCreated-F")
}

開始顯示:

override fun onStart() {
    super.onStart()

    Log.d("main", "onStart-F")
}

進入可互動狀態:

override fun onResume() {
    super.onResume()

    Log.d("main", "onResume-F")
}

離開畫面:

override fun onStop() {
    super.onStop()

    Log.d("main", "onStop-F")
}

View 被銷毀:

override fun onDestroyView() {
    super.onDestroyView()

    Log.d("main", "onDestroyView-F")
}

Fragment 本身被銷毀:

override fun onDestroy() {
    super.onDestroy()

    Log.d("main", "onDestroy-F")
}

最後離開 Activity:

override fun onDetach() {
    super.onDetach()

    Log.d("main", "onDetach-F")
}

參考資料:Android Developers-Fragment lifecycle


Fragment 生命週期可以這樣記

Fragment 建立
     │
     ▼
 onAttach()
     │
     ▼
 onCreate()
     │
     ▼
 onCreateView()
     │
     ▼
 onViewCreated()
     │
     ▼
 onStart()
     │
     ▼
 onResume()
     │
     ▼
   使用中
     │
     ▼
 onStop()
     │
     ▼
 onDestroyView()
     │
     ▼
 onDestroy()
     │
     ▼
 onDetach()

這個觀念之後非常重要。

因為:

Fragment 本身的生命週期與 Fragment 裡 View 的生命週期並不完全相同。

這也是為什麼後面學 ViewBinding、LiveData、Flow、Navigation Component 時,常常會看到 viewLifecycleOwner

參考資料:Android Developers-Fragment lifecycle


五、Activity 本身也有生命週期

今天的 MainActivity 同時也加入 Log。

override fun onStart() {
    super.onStart()
    Log.d("main", "onStart-M")
}

override fun onResume() {
    super.onResume()
    Log.d("main", "onResume-M")
}

override fun onRestart() {
    super.onRestart()
    Log.d("main", "onRestart-M")
}

override fun onPause() {
    super.onPause()
    Log.d("main", "onPause-M")
}

override fun onStop() {
    super.onStop()
    Log.d("main", "onStop-M")
}

override fun onDestroy() {
    super.onDestroy()
    Log.d("main", "onDestroy-M")
}

這樣就可以在 Logcat 同時觀察:

-M = MainActivity
-F = Fragment

也就是比較:

Activity Lifecycle
        VS
Fragment Lifecycle

這個實驗比單純背生命週期圖更容易理解。

參考資料:Android Developers-Activity lifecycle


六、Fragment 如何把資料傳回 Activity?

這次練習還有一個很有意思的地方。

Fragment 裡取得 Activity 的 TextView

val textViewMain =
    activity?.findViewById<TextView>(R.id.textView_mainData)

接著設定 Fragment 裡按鈕:

buttonData.setOnClickListener {

    val input = editTextInput.text.toString()

    val data = if (input.isEmpty())
        "no input data in fragment"
    else
        input

    textViewMain?.text = data
}

所以流程變成:

Fragment EditText
       │
       │ 使用者輸入
       ▼
   按下 Button
       │
       ▼
取得 input 字串
       │
       ▼
Activity TextView
       │
       ▼
顯示 Fragment 傳來的資料

這個範例直接證明:

Fragment 與 Activity 並不是完全獨立的兩個世界,它們可以互相溝通。

這裡是今天教材採用的直接操作方式;實務專案若資料互動越來越複雜,後續通常會進一步使用 Fragment Result API、共用 ViewModel 等方式降低耦合。

參考資料:Android Developers-Communicate with fragments


七、Fragment 之間怎麼傳資料?

接下來的第二個 Fragment 範例更加重要。

我們建立:

Fragment_1
Fragment_2
Fragment_3

三個 Fragment。

每個 Fragment 都提供:

newInstance()

例如 Fragment 1:

companion object {

    fun newInstance(
        param1: String,
        param2: String
    ): Fragment_1 {

        val fragment = Fragment_1()

        val args = Bundle()

        args.putString("param1", param1)
        args.putString("param2", param2)

        fragment.arguments = args

        return fragment
    }
}

這裡的核心就是:

Bundle()

我們把資料放進 Bundle:

args.putString("param1", param1)
args.putString("param2", param2)

再交給 Fragment:

fragment.arguments = args

參考資料:Android Developers-Pass data between destinations


八、Fragment 如何把 Bundle 資料拿出來?

Fragment1.kt 裡:

arguments?.getString("param1")?.let {
    textView.text = it
}

所以資料流其實是:

MainActivity
     │
     │ "Fragment 1"
     ▼
newInstance()
     │
     ▼
   Bundle
     │
     ▼
arguments
     │
     ▼
Fragment_1
     │
     ▼
 TextView

同樣的寫法也出現在 Fragment_2Fragment_3

例如 Fragment 2:

arguments?.getString("param1")?.let {
    textView.text = it
}

Fragment 3:

arguments?.getString("param1")?.let {
    textView.text = it
}

因此只要 MainActivity 傳不同文字,就可以讓相同概念的 Fragment 顯示不同資料。

參考資料:Android Developers-Fragments


九、FragmentManager:Fragment 的管理員

真正把 Fragment 放進 Activity 的關鍵,在今天最後的 MainActivity

首先建立 Fragment:

frag1 = Fragment_1.newInstance(
    "Fragment 1",
    "no data"
)

接著:

val fragTransit =
    supportFragmentManager.beginTransaction()

這裡出現兩個重要角色:

FragmentManager
FragmentTransaction

可以理解成:

FragmentManager
      │
      │ 管理
      ▼
FragmentTransaction
      │
      │ 執行
      ▼
 add / replace / remove
      │
      ▼
   Fragment

參考資料:Android Developers-FragmentManager


十、把 Fragment 放進 FrameLayout

今天程式:

fragTransit.add(
    R.id.frameLayout_id,
    frag1,
    "frag-1"
)

意思就是:

把 frag1
   ↓
加入
   ↓
R.id.frameLayout_id

所以 XML 裡面的 FrameLayout 可以想成:

Fragment 的舞台。

MainActivity
│
├── Button Frag-1
├── Button Frag-2
├── Button Frag-3
├── EditText
│
└── FrameLayout
       │
       └── Fragment 顯示區

從你今天的實際執行畫面,也已經可以看到上方 Frag-1 / Frag-2 / Frag-3,下方則是 Fragment 的內容顯示區。

參考資料:Android Developers-Fragment transactions


十一、addToBackStack() 是做什麼?

今天程式還有:

fragTransit.addToBackStack(null)

這個非常重要。

它的用途是把 Fragment 的 Transaction 放進返回堆疊:

Fragment 1
    ↓
Fragment 2
    ↓
Fragment 3

如果 Transaction 有加入 Back Stack,使用者按 Android 的返回鍵時,就能依交易紀錄返回先前狀態。

最後:

fragTransit.commit()

才是真正送出這次 Fragment Transaction。

因此完整寫法:

val fragTransit =
    supportFragmentManager.beginTransaction()

fragTransit.add(
    R.id.frameLayout_id,
    frag1,
    "frag-1"
)

fragTransit.addToBackStack(null)

fragTransit.commit()

可以簡單記成:

beginTransaction()
       ↓
      add()
       ↓
addToBackStack()
       ↓
     commit()

參考資料:Android Developers-Fragment transactions


十二、Firebase:昨天的內容今天也繼續延伸

今天提供的另一組程式仍然保留 Firebase Activity 的練習。

FirebaseActivity.kt 目前先建立一個 ListView,產生 20 筆資料,再使用 SimpleAdapter 顯示。

資料建立:

val data = ArrayList<Map<String, String>>()

for (i in 1..20) {

    val item = HashMap<String, String>()

    item["title"] = "Item $i"
    item["subtitle"] = "Sub Item $i"

    data.add(item)
}

接著建立:

val adapter = SimpleAdapter(
    this,
    data,
    android.R.layout.simple_list_item_2,
    arrayOf("title", "subtitle"),
    intArrayOf(
        android.R.id.text1,
        android.R.id.text2
    )
)

listView.adapter = adapter

目前這部分資料仍然是本機建立的測試資料,你提供的程式碼尚未看到真正讀取 Firebase 資料庫的程式,因此文章這裡不能直接說成「已從 Firebase 抓回 20 筆資料」。

參考資料:Firebase Android 官方文件


十三、FloatingActionButton

今天 Firebase Activity 還加入:

val fab =
    findViewById<FloatingActionButton>(
        R.id.floatingButton_id
    )

並設定:

fab.setOnClickListener {

    Toast.makeText(
        this,
        "Floating button clicked",
        Toast.LENGTH_SHORT
    ).show()
}

所以目前:

FloatingActionButton
        │
        ▼
      Click
        │
        ▼
      Toast
        │
        ▼
"Floating button clicked"

這也替後續「按 + 新增 Firebase 資料」預留了一個非常合理的 UI 入口。

參考資料:Material Design-Floating Action Button


十四、今天的完整架構

把今天練習整理起來,大致可以畫成:

                    MainActivity
                         │
          ┌──────────────┼──────────────┐
          │              │              │
      [Frag-1]       [Frag-2]       [Frag-3]
          │              │              │
          └──────────────┼──────────────┘
                         │
                         ▼
                FragmentManager
                         │
                         ▼
               FragmentTransaction
                         │
                ┌────────┴────────┐
                │                 │
              add()            replace()
                │                 │
                └────────┬────────┘
                         ▼
                    FrameLayout
                         │
          ┌──────────────┼──────────────┐
          ▼              ▼              ▼
     Fragment_1     Fragment_2     Fragment_3
          │
          ├── TextView
          ├── ImageView
          ├── EditText
          └── Button
               │
               ▼
        Activity TextView

這也就是 Fragment 的核心精神:

Activity 負責整體畫面,Fragment 負責可以獨立管理、替換的局部 UI。

參考資料:Android Developers-Fragments


十五、今天學到了什麼?

今天不是單純「學會建立 Fragment」,而是開始把 Android UI 從單一 Activity 拆成比較接近真正 App 的架構。

今天實際碰到的重點包括:

  1. ActivityFragment 的差異
  2. Fragment() 的建立方式
  3. onCreateView()inflate()
  4. Fragment 自己操作 Button / TextView / ImageView / EditText
  5. Fragment Lifecycle
  6. Activity Lifecycle 與 Fragment Lifecycle 的比較
  7. Fragment → Activity 資料傳遞
  8. Bundlearguments
  9. newInstance() Factory Method
  10. FragmentManager
  11. FragmentTransaction
  12. add()
  13. addToBackStack()
  14. commit()
  15. 多個 Fragment 的基本架構
  16. ListView + SimpleAdapter
  17. FloatingActionButton

今天的 MainActivity 已經建立三個 Fragment 按鈕以及 Fragment_1 的初始 Transaction;目前提供的這份程式尚未看到三個按鈕的 setOnClickListener 切換實作,因此下一步正好可以繼續完成真正的 Frag-1 → Frag-2 → Frag-3 動態切換。

參考資料:Android Developers-Fragments


Day 27 建議正式標題

Day 27|Android Fragment 實戰:生命週期、FragmentManager 與 Activity 資料傳遞

副標題可以使用:

一個 Activity 不只一個畫面!從 Fragment Lifecycle 到三頁動態切換

今天最大的收穫,就是開始理解 Android App 並不一定需要「換畫面就開 Activity」。透過 Fragment,我們可以將 UI 拆成多個可以獨立管理的區塊,再利用 FragmentManager 動態加入、替換及管理。這個觀念也是接下來可能能會的學習 Navigation Component、BottomNavigationView、ViewModel,甚至更完整 App 架構的重要基礎。

參考資料:Android Developers-Fragment 官方指南


上一篇
RecyclerView 進階實戰:從商品清單到飲料點餐系統
下一篇
Android Fragment 實戰進階:BottomNavigationView 打造 App 底部導覽列
系列文
Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言